// 这是一个 Vue 的内置组件，用于缓存动态组件或路由组件的状态，以提高应用程序的性能。keep-alive 组件会将符合条件的组件实例缓存起来，在下一次需要使用时直接从缓存中取出，而不是重新创建新的组件实例

// 在源码中，主要包含以下几个部分：

// 组件属性：包括 include、exclude 和 max 属性，用于控制哪些组件需要被缓存、哪些组件需要排除以及最大缓存数量等。
// 生命周期钩子函数：包括 created 和 destroyed 钩子函数，用于创建和销毁缓存对象。
// 钩子函数：activated和deactivated，
// 渲染函数：用于渲染 keep-alive 组件的子节点，并根据条件进行缓存或取出缓存中的组件实例。

// 判断一个值是否定义
const isDef = (v) => v !== undefined

export default {
	name: 'keep-alive',
	abstract: true, // 抽象组件，不会渲染真实的 DOM 元素

	props: {
		include: patternTypes, // 可传字符串、正则表达式、数组，名称匹配成功的组件会被缓存
		exclude: patternTypes, // 可传字符串、正则表达式、数组，名称匹配成功的组件不会被缓存
		max: [String, Number] // 可传数字，限制缓存组件的最大数量,默认为10
	},

	methods: {
		cacheVNode() {
			//首先，从组件实例的属性中获取相关的变量，包括 cache（缓存对象）、keys（缓存键列表）、vnodeToCache（要缓存的虚拟节点）、keyToCache（要缓存的键）。
			const { cache, keys, vnodeToCache, keyToCache } = this
			//如果存在要缓存的虚拟节点 vnodeToCache，则将其相关信息存储到缓存对象中。这包括组件的名称、标签和组件实例。
			if (vnodeToCache) {
				const { tag, componentInstance, componentOptions } = vnodeToCache
				//将要缓存的键 keyToCache 添加到缓存键列表 keys 中。
				cache[keyToCache] = {
					name: _getComponentName(componentOptions),
					tag,
					componentInstance
				}
				keys.push(keyToCache)
				// 如果设置了最大缓存数 max，并且当前缓存键列表的长度超过了最大缓存数，则会清理掉最旧的缓存项。这里调用了 pruneCacheEntry 方法来执行清理操作。
				if (this.max && keys.length > parseInt(this.max)) {
					pruneCacheEntry(cache, keys[0], keys, this._vnode)
				}
				//最后，将 vnodeToCache 设置为 null，表示缓存操作完成。
				this.vnodeToCache = null
			}
		}
	},

	created() {
		// 创建一个缓存组件实例的对象
		this.cache = Object.create(null)
		// 创建一个缓存的组件实例的键列表
		this.keys = []
	},
	//实例销毁后
	destroyed() {
		for (const key in this.cache) {
			// 销毁组件实例时，清空缓存
			pruneCacheEntry(this.cache, key, this.keys)
		}
	},
	//在mounted钩子函数中观测 include 和 exclude 的变化
	mounted() {
		// 监听 include 和 exclude 属性的变化，动态调整缓存列表
		this.$watch('include', (val) => {
			pruneCache(this, (name) => matches(val, name))
		})
		this.$watch('exclude', (val) => {
			pruneCache(this, (name) => !matches(val, name))
		})
	},
	activated() {
		// 当缓存的组件被激活时，重新挂载组件实例
		this.keepAlive = true
		const { cache, keys } = this
		for (let i = 0; i < keys.length; i++) {
			const key = keys[i]
			const cachedNode = cache[key]
			if (cachedNode && cachedNode.componentInstance) {
				cachedNode.componentInstance._activated = true
				if (cachedNode.data.keepAlive) {
					activateChildComponent(cachedNode.componentInstance, true /* direct */)
				}
			}
		}
	},

	deactivated() {
		// 当缓存的组件被停用时，禁用组件实例
		this.keepAlive = false
		const { cache, keys } = this
		for (let i = 0; i < keys.length; i++) {
			const key = keys[i]
			const cachedNode = cache[key]
			if (cachedNode && cachedNode.componentInstance) {
				cachedNode.componentInstance._activated = false
				deactivateChildComponent(cachedNode.componentInstance, true /* direct */)
			}
		}
	},
	render() {
		/* 获取默认插槽中的第一个组件节点 */
		const slot = this.$slots.default
		const vnode = getFirstComponentChild(slot)
		/* 获取该组件节点的componentOptions */
		const componentOptions = vnode && vnode.componentOptions
		if (componentOptions) {
			// 检查组件是否符合缓存条件  获取该组件节点的名称，优先获取组件的name字段，如果name不存在则获取组件的tag
			const name = getComponentName(componentOptions)
			const { include, exclude } = this
			if (
				// 不在 include 匹配列表中
				(include && (!name || !matches(include, name))) ||
				// 在 exclude 匹配列表中
				(exclude && name && matches(exclude, name))
			) {
				return vnode // 不需要进行缓存，直接返回虚拟节点或插槽内容
			}

			const { cache, keys } = this
			/* 获取组件的key值 */
			const key =
				vnode.key == null
					? // 相同的构造函数可能会被注册为不同的局部组件，因此仅使用 cid 并不足够 (#3269)
					  componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
					: vnode.key
			/*  拿到key值后去this.cache对象中去寻找是否有该值，如果有则表示该组件有缓存，即命中缓存 */
			if (cache[key]) {
				// 如果缓存中已经存在该组件实例，则更新其位置，使其成为最新的缓存项，直接使用缓存中的实例
				vnode.componentInstance = cache[key].componentInstance
				// 将当前组件实例的键移到缓存列表末尾，表示该实例最近被访问过,以保持最新使用的组件在最后面, 调整该组件key的顺序，将其从原来的地方删掉并重新放在最后一个,直接从缓存中拿 vnode 的组件实例，此时重新调整该组件key的顺序，将其从原来的地方删掉并重新放在this.keys中最后一个
				remove(keys, key)
				keys.push(key)
			} else {
				//this.cache对象中没有该key值的情况
				// 如果缓存中不存在该组件实例，则创建新的缓存项并添加到缓存对象和键列表中，将其添加到缓存列表中，并将其键移到列表末尾,
				cache[key] = vnode
				keys.push(key)
				//如果设置了最大缓存数量，并且当前缓存数量超过最大值，则移除最早的缓存项
				if (this.max && keys.length > parseInt(this.max)) {
					pruneCacheEntry(cache, keys[0], keys, this._vnode)
				}
				//表明该组件还没有被缓存过，则以该组件的key为键，组件vnode为值，将其存入this.cache中，并且把key存入this.keys中

				// 此时再判断this.keys中缓存组件的数量是否超过了设置的最大缓存数量值this.max，如果超过了，则把第一个缓存组件删掉
			}

			//  设置标记，表示该组件是被 keep-alive 缓存的
			vnode.data.keepAlive = true
		}
		return vnode || (slot && slot[0]) //返回组件 vnode 或插槽内容的第一个节点
	}
}

// 以下这些辅助函数在 keep-alive 组件的实现中起到了关键作用，用于处理组件的缓存和更新逻辑。

// 获取组件的名称,优先获取组件选项中的名称
function getComponentName(opts) {
	//如果不存在则获取组件的标签名 (opts.tag)
	return opts && (opts.Ctor.options.name || opts.tag)
}
//检查给定的名称是否与模式匹配。模式可以是字符串、字符串数组或正则表达式。如果模式是字符串，则使用逗号分隔多个名称。
function matches(pattern, name) {
	if (Array.isArray(pattern)) {
		return pattern.indexOf(name) > -1
	} else if (typeof pattern === 'string') {
		return pattern.split(',').indexOf(name) > -1
	} else if (isRegExp(pattern)) {
		return pattern.test(name)
	}
	/* istanbul ignore next */
	return false
}
// 根据过滤条件清理缓存中的组件实例。遍历缓存对象，对于每个缓存项，获取组件名称并应用过滤条件，如果不符合条件则清理该缓存项。如果include 或exclude 发生了变化，即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化，那么就执行pruneCache函数,
//在该函数内对this.cache对象进行遍历，取出每一项的name值，用其与新的缓存规则进行匹配，如果匹配不上，则表示在新的缓存规则下该组件已经不需要被缓存，则调用pruneCacheEntry函数将其从this.cache对象剔除即可
function pruneCache(keepAliveInstance, filter) {
	const { cache, keys, _vnode } = keepAliveInstance
	for (const key in cache) {
		const cachedNode = cache[key]
		if (cachedNode) {
			const name = getComponentName(cachedNode.componentOptions)
			if (name && !filter(name)) {
				pruneCacheEntry(cache, key, keys, _vnode)
			}
		}
	}
}
// 清理指定的缓存项。销毁组件实例并将缓存项设置为 null，同时从键列表中移除该键。
function pruneCacheEntry(cache, key, keys, current) {
	const cached = cache[key]
	/* 判断当前没有处于被渲染状态的组件，将其销毁*/
	if (cached && (!current || cached.tag !== current.tag)) {
		cached.componentInstance.$destroy()
	}
	cache[key] = null
	remove(keys, key)
}
// 获取第一个组件类型的子节点。遍历子节点数组，返回第一个满足条件的组件类型节点。
function getFirstComponentChild(children) {
	if (Array.isArray(children)) {
		for (let i = 0; i < children.length; i++) {
			const c = children[i]
			if (isDef(c) && (isDef(c.componentOptions) || c.isComment)) {
				return c
			}
		}
	}
}

function remove(arr, item) {
	if (arr.length) {
		const index = arr.indexOf(item)
		if (index > -1) {
			return arr.splice(index, 1)
		}
	}
}
//其中包含了组件的属性、生命周期钩子函数、辅助函数，用于实现组件的缓存和更新逻辑。
// 这是一个抽象组件，它的作用是缓存动态组件或组件树，以避免多次重复渲染。在使用该组件时，只需要将需要缓存的组件包裹在 <keep-alive> 标签中即可。

//缓存后如何获取数据

// beforeRouteEnter
// actived

// 每次组件渲染的时候，都会执行beforeRouteEnter
function beforeRouteEnter(to, from, next) {
	next((vm) => {
		console.log(vm)
		// 每次进入路由执行
		vm.getData() // 获取数据
	})
}

// 在keep-alive缓存的组件被激活的时候，都会执行actived钩子
function activated() {
	this.getData() // 获取数据
}

//参考
// https://zhuanlan.zhihu.com/p/518960937?utm_id=0
